Long only 1/n portfolio#
import pandas as pd
pd.options.plotting.backend = "plotly"
import yfinance as yf
from cvx.simulator.builder import builder
from cvx.simulator.grid import resample_index
data = yf.download(tickers = "SPY AAPL GOOG MSFT", # list of tickers
period = "10y", # time period
interval = "1d", # trading interval
prepost = False, # download pre/post market hours data?
repair = True) # repair obvious price errors e.g. 100x?
[ 0%% ]
[**********************50%% ] 2 of 4 completed
[**********************75%%********** ] 3 of 4 completed
[*********************100%%**********************] 4 of 4 completed
prices = data["Adj Close"]
capital = 1e6
b = builder(prices=prices, initial_cash=capital)
for time, state in b:
# each day we invest a quarter of the capital in the assets
b[time[-1]] = 0.25 * state.nav / state.prices
portfolio = b.build()
portfolio.profit.cumsum().plot()
/home/runner/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning: The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
v = v.dt.to_pydatetime()
portfolio.nav.plot()
/home/runner/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:
The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
Rebalancing#
Usually we would not execute on a daily basis but rather rebalance every week, month or quarter. There are two approaches to deal with this problem in cvxsimulator.
Resample the existing daily portfolio (helpful to see effect of your hesitated trading)
Trade only on days that are within a predefined grid (most flexible if you have a rather irregular grid)
Resample an existing portfolio#
portfolio_resampled = portfolio.resample(rule="M")
frame = pd.DataFrame({"original": portfolio.nav, "monthly": portfolio_resampled.nav})
frame
| original | monthly | |
|---|---|---|
| Date | ||
| 2013-10-18 | 1.000000e+06 | 1.000000e+06 |
| 2013-10-21 | 1.004350e+06 | 1.004350e+06 |
| 2013-10-22 | 1.003071e+06 | 1.003049e+06 |
| 2013-10-23 | 1.004457e+06 | 1.004515e+06 |
| 2013-10-24 | 1.006892e+06 | 1.007026e+06 |
| ... | ... | ... |
| 2023-10-11 | 7.401893e+06 | 7.383126e+06 |
| 2023-10-12 | 7.374549e+06 | 7.355692e+06 |
| 2023-10-13 | 7.304823e+06 | 7.285796e+06 |
| 2023-10-16 | 7.375240e+06 | 7.356042e+06 |
| 2023-10-17 | 7.362305e+06 | 7.343185e+06 |
2516 rows × 2 columns
print(portfolio_resampled.stocks)
AAPL GOOG MSFT SPY
Date
2013-10-18 15761.003961 9924.246904 8539.412661 1719.976598
2013-10-21 15761.003961 9924.246904 8539.412661 1719.976598
2013-10-22 15761.003961 9924.246904 8539.412661 1719.976598
2013-10-23 15761.003961 9924.246904 8539.412661 1719.976598
2013-10-24 15761.003961 9924.246904 8539.412661 1719.976598
... ... ... ... ...
2023-10-11 10296.570678 13235.401195 5559.444447 4186.724311
2023-10-12 10296.570678 13235.401195 5559.444447 4186.724311
2023-10-13 10296.570678 13235.401195 5559.444447 4186.724311
2023-10-16 10296.570678 13235.401195 5559.444447 4186.724311
2023-10-17 10296.570678 13235.401195 5559.444447 4186.724311
[2516 rows x 4 columns]
# almost hard to see that difference between the original and resampled portfolio
frame.plot()
/home/runner/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:
The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
# number of shares traded
portfolio_resampled.trades_stocks.iloc[1:].plot()
/home/runner/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:
The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
Trade only days in predefined grid#
b = builder(prices=prices, initial_cash=capital)
# define a grid
grid = resample_index(prices.index, rule="M")
for time, state in b:
# each day we invest a quarter of the capital in the assets
if time[-1] in grid:
b[time[-1]] = 0.25 * state.nav / state.prices
else:
# forward fill an existing position
b[time[-1]] = b[time[-2]]
portfolio = b.build()
portfolio.nav.plot()
/home/runner/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:
The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
# Trading only once a month can lead to days where 150k had to be reallocated
portfolio.turnover.iloc[1:].plot()
/home/runner/work/cvxmarkowitz/cvxmarkowitz/.venv/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:
The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
Why not resampling the prices?#
I don’t believe in bringing the prices to a monthly grid. This would render it hard to construct signals given the sparse grid. We stay on a daily grid and trade once a month.